﻿using System;
using System.ComponentModel;
using System.Reflection;
using System.Text;

namespace Kyoh.Lib.Component.Arguments
{
    /// <summary>引数を表すプロパティに対する装飾属性を定義します。</summary>
    [AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
    public partial class ArgumentPropertyAttribute : ArgumentAttributeBase
    {
        /// <summary>引数名を取得・設定します。</summary>
        public virtual string ArgumentName { get { return this.ArgumentNameCore; } }
        /// <summary>引数名を取得・設定します。</summary>
        protected string ArgumentNameCore { get; private set; }
        /// <summary>引数名の省略語を取得・設定します。</summary>
        public virtual string Abbreviation { get { return this.AbbreviationCore; } }
        /// <summary>引数名の省略語を取得・設定します。</summary>
        protected string AbbreviationCore { get; private set; }

        /// <summary>必須の引数であるかどうかを取得・設定します。</summary>
        public bool Required { get; set; }
        /// <summary>引数名に指定された文字列の、大文字/小文字を区別しないかどうかを取得・設定します。</summary>
        public bool? IgnoreCase { get; set; }

        /// <summary>引数が対応するプロパティの情報を取得します。</summary>
        public PropertyInfo Property { get; private set; }
        /// <summary>引数が対応するプロパティの型を取得します。型がNULL許容型の場合、基になる型を返します。</summary>
        public Type PropertyType
        {
            get
            {
                Type u = Nullable.GetUnderlyingType(this.Property.PropertyType);
                if (u != null)
                    return u;
                else
                    return this.Property.PropertyType;
            }
        }
        private ConstructorInfo constructor;
        private TypeConverter converter;

        /// <summary>引数名の表示用文字列を取得します。</summary>
        private string Key { get; set; }
        /// <summary>引数名の表示用文字列の長さを取得します。</summary>
        private int KeyLength { get; set; }

        /// <summary>接頭文字の表示用文字列を取得します。</summary>
        private string Prefix { get; set; }
        /// <summary>接頭文字の表示用文字列の長さを取得します。</summary>
        private int PrefixLength { get; set; }
        /// <summary>区切り文字の表示用文字列を取得します。</summary>
        private string Separator { get; set; }
        /// <summary>区切り文字の表示用文字列の長さを取得します。</summary>
        private int SeparatorLength { get; set; }

        /// <summary><see cref="Reffer"/> がコールされたかどうかを取得します。</summary>
        private bool IsReffered { get; set; }

        /// <summary><see cref="ArgumentPropertyAttribute"/> クラスの新しいインスタンスを初期化します。</summary>
        /// <param name="argumentName">引数名を指定します。</param>
        public ArgumentPropertyAttribute(string argumentName) : this(argumentName, null) { }
        /// <summary><see cref="ArgumentPropertyAttribute"/> クラスの新しいインスタンスを省略語と共に作成します。</summary>
        /// <param name="argumentName">引数名を指定します。</param>
        /// <param name="abbreviation">引数名の省略語を指定します。</param>
        public ArgumentPropertyAttribute(string argumentName, string abbreviation)
        {
            this.ArgumentNameCore = (argumentName ?? string.Empty).Trim();
            this.AbbreviationCore = (abbreviation ?? string.Empty).Trim();

            this.Key = (this.Abbreviation == string.Empty ? this.ArgumentName : (this.Abbreviation + "|")) + this.ArgumentName;
            this.KeyLength = this.Key.Length;
        }

        /// <summary>引数名が、このオブジェクトに指定された引数名と一致するかどうかを取得します。</summary>
        /// <param name="argumentName">引数名を指定します。</param>
        /// <returns>引数名が、このオブジェクトに指定された引数名と一致する場合は <c>true</c>。それ以外の場合は <c>false</c></returns>
        public virtual bool Match(string argumentName)
        {
            return string.Equals(argumentName, this.ArgumentName, this.IgnoreCase.Value ? StringComparison.InvariantCultureIgnoreCase : StringComparison.InvariantCulture) ||
                ( this.Abbreviation != string.Empty
                && string.Equals(argumentName, this.Abbreviation, this.IgnoreCase.Value ? StringComparison.InvariantCultureIgnoreCase : StringComparison.InvariantCulture) );
        }

        /// <summary>引数に、クラスとプロパティを関連づけます。</summary>
        /// <exception cref="InvalidArgumentException">引数の定義が不正です。<see cref="InvalidArgumentException.Invalidity"/> プロパティで不正な定義の原因を特定できます。</exception>
        /// <exception cref="InvalidPrefixException">引数の接頭語に空文字、または空白のみが指定されています。</exception>
        /// <exception cref="InvalidSeparatorException">引数の区切り文字に空文字、または空白のみが指定されています。</exception>
        private void Reffer(ArgumentObjectAttribute obj, PropertyInfo prop)
        {
            if (this.IsReffered)
                return;

            this.Property = prop;
            this.constructor = this.PropertyType.GetConstructor(Type.EmptyTypes);

            this.converter = TypeDescriptor.GetConverter(this.PropertyType);
            // TypeConverterAttributeを、プロパティから取得する
            try
            {
                foreach (TypeConverterAttribute attr in prop.GetCustomAttributes(typeof(TypeConverterAttribute), true))
                {
                    try { this.converter = (TypeConverter)Type.GetType(attr.ConverterTypeName).GetConstructor(Type.EmptyTypes).Invoke(null); }
                    catch { }
                }
            }
            catch { }

            if (this.converter == null || !this.converter.CanConvertFrom(typeof(string)) || !this.converter.CanConvertTo(typeof(string)))
                throw new InvalidArgumentException(ArgumentInvalidity.PropertyType);

            obj = obj ?? ArgumentObjectAttribute.Default;

            if (this.IgnoreCase == null || this.IgnoreCase.HasValue == false)
                this.IgnoreCase = obj.IgnoreCase;

            if (this.prefixes == null)
            {
                if (obj.prefixes != null)
                    this.prefixes = obj.prefixes;
                else
                    this.prefixes = ArgumentPropertyAttribute.GetDefaultPrefixes();
            }
            if (this.prefixes.Length == 0)
                throw new InvalidPrefixException() { ArgumentName = this.ArgumentName, PropertyName = prop.Name, Prefix = new string(this.prefixes) };
            else if (this.prefixes.Length == 1)
                this.Prefix = this.prefixes[0].ToString();
            else
                this.Prefix = "[" + new string(this.prefixes) + "]";
            this.PrefixLength = this.Prefix.Length;

            if (this.separators == null)
            {
                if (obj.separators != null)
                    this.separators = obj.separators;
                else
                    this.separators = ArgumentPropertyAttribute.GetDefaultSeparators();
            }
            if (this.PropertyType == typeof(bool))
                this.Separator = "...";
            else if (this.separators.Length == 0)
                throw new InvalidSeparatorException() { ArgumentName = this.ArgumentName, PropertyName = prop.Name, Separator = new string(this.separators) };
            else if(this.separators.Length == 1)
                this.Separator = this.separators[0].ToString();
            else
                this.Separator = "[" + new string(this.separators) + "]";
            this.SeparatorLength = this.Separator.Length;

            // 引数名のチェック
            if (this.ArgumentName == string.Empty)
                throw new InvalidArgumentException(ArgumentInvalidity.EmptyName) { ArgumentName = this.ArgumentName, PropertyName = prop.Name };

            foreach (var s in this.separators)
                if (this.ArgumentName.IndexOf(s) >= 0)
                    throw new InvalidArgumentException(ArgumentInvalidity.NameContainsSeparator) { ArgumentName = this.ArgumentName, PropertyName = prop.Name };
                else if (this.Abbreviation.IndexOf(s) >= 0)
                    throw new InvalidArgumentException(ArgumentInvalidity.NameContainsSeparator) { ArgumentName = this.Abbreviation, PropertyName = prop.Name };
            
            this.IsReffered = true;
        }

        private string Argument(object obj)
        {
            var message = new StringBuilder();

            // 引数の接頭語を追加する
            message.Append(this.prefixes[0]);

            // 引数名を追加する
            message.Append(this.Key);

            if (this.PropertyType != typeof(bool))
            {
                // 引数名と値の間のセパレータを追加する
                message.Append(this.separators[0]);

                // 値を追加する
                message.Append(this.converter.ConvertToString(this.Property.GetValue(obj, null)));
            }

            return message.ToString();
        }

        private string DetailUsage(int prefixLength, int keyLength, int separatorLength)
        {
            var message = new StringBuilder();

            // 引数の接頭語を追加する
            message.Append(Prefix);

            // 引数名を追加する
            message.Append(this.Key);

            // 接頭語の長さ調整
            if (this.PrefixLength < prefixLength)
                message.Append(new string(' ', prefixLength - this.PrefixLength));

            // 引数名の長さ調整
            if (this.KeyLength < keyLength)
                message.Append(new string(' ', keyLength - this.KeyLength));

            // 引数名と値の間のセパレータを追加する
            message.Append(Separator);

            // セパレータの長さ調整
            if (this.SeparatorLength < separatorLength)
                message.Append(new string(' ', separatorLength - this.SeparatorLength));

            // オプション引数であることを示す文字列を追加する
            if (!this.Required)
                message.Append("(option)");

            // 型を追加する
            message.Append('{' + this.PropertyType.Name + '}');

            // 説明を追加する
            foreach (DescriptionAttribute description in this.Property.GetCustomAttributes(typeof(DescriptionAttribute), true))
                message.Append(description.Description.Replace("\n", string.Empty));

            return message.ToString();
        }

        private string Usage()
        {
            var message = new StringBuilder();

            // オプション引数であることを示す文字列を追加する
            if (!this.Required)
                message.Append('(');

            // 引数の接頭語を追加する
            message.Append(Prefix);

            // 引数名を追加する
            message.Append(this.Key);

            if (this.PropertyType != typeof(bool))
            {
                // 引数名と値の間のセパレータを追加する
                message.Append(Separator);

                // 型を追加する
                message.Append('{' + this.PropertyType.Name + '}');
            }

            if (!this.Required)
                message.Append(")?");

            return message.ToString();
        }
    }
}